﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Pf.Common;

namespace Pf.BusinessLogic.CharacterCreation.DetermineAbilityScores
{
    /// <summary>
    /// Businesslogic for Determine Ability Scores. Handles all data gathering and workflow.
    /// </summary>
    public class DetermineAbilityScores
    {
        /// <summary>
        /// Status of this workflow.
        /// </summary>
        public DetermineAbilityScoresStatus Status { get; private set; }

        /// <summary>
        /// The base method type
        /// </summary>
        public MethodTypeEnum MethodType { get; private set; }
        
        /// <summary>
        /// Further specificatoin of Randomized method type
        /// </summary>
        public RandomizedTypeEnum RandomizedType { get; private set; }
        
        /// <summary>
        /// Number of points for Point-buy method.
        /// </summary>
        public AbilityScorePointsEnum AbilityScorePoints { get; private set; }
        
        /// <summary>
        /// Scores are pre-assigned when randomized method is used.
        /// </summary>
        public bool? PreAssigned { get; private set; }

        /// <summary>
        /// The final assigned ability scores.
        /// </summary>
        public AbilityValues AbilityScores { get; private set; }

        /// <summary>
        /// Scores to be assigned to abilities.
        /// </summary>
        public IEnumerable<int> AssignableScores { get; private set; }

        private void CheckStatus(string methodName, DetermineAbilityScoresStatus status)
        {
            if(Status != status)
                throw new InvalidOperationException(String.Format("Method {0} should only be called when status it {1}.", methodName, Enum.GetName(typeof(DetermineAbilityScoresStatus), status)));
        }

        private void RollAndPreAssign(Func<int> randomizationMethod)
        {
            AbilityScores = new AbilityValues();
            AbilityScores.SetAll(() => randomizationMethod());
        }

        private void RollNotPreAssigned(Func<int> randomizationMethod)
        {
            var list = new List<int>();
            for (int i = 0; i < 6; i++)
            {
                list.Add(randomizationMethod());
            }            
            AssignableScores = list;
        }

        private void CheckScoresBoundaries(AbilityValues abilityScores)
        {
            if (abilityScores.AsEnumerable().Max() > 18)
                throw new ArgumentException("Ability scores cannot be higher than 18.");
            if (abilityScores.AsEnumerable().Min() < 3)
                throw new ArgumentException("Ability scores cannot be lower than 3.");
        }

        public DetermineAbilityScores()
        {
            Status = DetermineAbilityScoresStatus.Initialized;
        }

        /// <summary>
        /// Determine the base method type for determining ability scores.
        /// </summary>
        /// <param name="methodType">Method type</param>
        public void DetermineMethodType(MethodTypeEnum methodType)
        {
            MethodType = methodType;
            
            // Reset values
            RandomizedType = RandomizedTypeEnum.Unknown;
            AbilityScorePoints = AbilityScorePointsEnum.Unknown;
            PreAssigned = null;
            switch (MethodType)
            {
                case MethodTypeEnum.Randomized: Status = DetermineAbilityScoresStatus.CanChooseRandomizedTypeAndPreAssigned; break;
                case MethodTypeEnum.PointBuy:   Status = DetermineAbilityScoresStatus.CanDetermineAmountOfAbilityScorePoints; break;
                case MethodTypeEnum.Manual:     Status = DetermineAbilityScoresStatus.CanAssignScores; break;
            }
        }               

        /// <summary>
        /// Determine the Randomized method type and whether the ability scores should be pre-assigned. 
        /// If the method is not dice-pool, the scores are determined randomly now.
        /// If the scores are not pre-assigned, the scores can be distributed among the abilities, using the AssignableScores list.
        /// If the scores are pre-assigned, the assigned scores can now be committed.
        /// If dice-pool is chosen, the amount of dice for each ability can now be determined.
        /// </summary>
        /// <param name="randomizedType">Method of randomization</param>
        /// <param name="preAssigned">Ability scores are pre-assigned or not.</param>
        public void DetermineRandomizedType(RandomizedTypeEnum randomizedType, bool preAssigned)
        {                                  
            CheckStatus("DetermineRandomizedType", DetermineAbilityScoresStatus.CanChooseRandomizedTypeAndPreAssigned);
            
            if (randomizedType == RandomizedTypeEnum.DicePool && preAssigned == false)
                throw new ArgumentException("When randomizedType is DicePool, preAssigned should be true.");

            RandomizedType = randomizedType;
            PreAssigned = preAssigned;
            if (RandomizedType != RandomizedTypeEnum.DicePool)
            {
                var dice = new Dice();
                if (preAssigned)
                {
                    switch (RandomizedType)
                    {
                        case RandomizedTypeEnum.Standard: RollAndPreAssign(() => dice.BestThree()); break;
                        case RandomizedTypeEnum.Classic: RollAndPreAssign(() => dice.Roll(3, 6)); break;
                        case RandomizedTypeEnum.Heroic: RollAndPreAssign(() => dice.Roll(2, 6) + 6); break;
                    }
                    Status = DetermineAbilityScoresStatus.CanCommitOrRollback;
                }
                else
                {
                    switch (RandomizedType)
                    {
                        case RandomizedTypeEnum.Standard: RollNotPreAssigned(() => dice.BestThree()); break;
                        case RandomizedTypeEnum.Classic: RollNotPreAssigned(() => dice.Roll(3, 6)); break;
                        case RandomizedTypeEnum.Heroic: RollNotPreAssigned(() => dice.Roll(2, 6) + 6); break;
                    }
                    Status = DetermineAbilityScoresStatus.CanAssignScores;
                }
            }
            else
            {
                Status = DetermineAbilityScoresStatus.CanDetermineDicePoolSizeAndDistribution;
            }
        }

        /// <summary>
        /// Determines the amount of points to use with the point-buy method.
        /// </summary>
        /// <param name="points">Amount of points</param>
        public void DetermineAmountOfAbilityScorePoints(AbilityScorePointsEnum points)
        {
            CheckStatus("DetermineAmountOfAbilityScorePoints", DetermineAbilityScoresStatus.CanDetermineAmountOfAbilityScorePoints);
            
            AbilityScorePoints = points;

            Status = DetermineAbilityScoresStatus.CanBuyAbilityScores;
        }

        /// <summary>
        /// Determine the poolsize for DicePool method.
        /// </summary>
        /// <param name="poolSize">The pool size</param>
        /// <param name="abilityScoreDistribution">The distribution</param>        
        public void DetermineDicePoolSizeAndDistribution(PoolSizeEnum poolSize, AbilityValues abilityScoreDistribution)
        {
            CheckStatus("DetermineDicePoolSizeAndDistribution", DetermineAbilityScoresStatus.CanDetermineDicePoolSizeAndDistribution);

            if (abilityScoreDistribution == null)
                throw new ArgumentNullException("abilityScoreDistribution");                       

            if ((int)poolSize != abilityScoreDistribution.AsEnumerable().Sum())
                throw new ArgumentException("Poolsize should be equal to the sum of the distribution values.");

            if (abilityScoreDistribution.AsEnumerable().Min() < 3)
                throw new ArgumentException("Every ability should have at least 3 dice assigned to it.");
            var dice = new Dice();
            AbilityScores.Strength = dice.BestThree(abilityScoreDistribution.Strength);
            AbilityScores.Dexterity = dice.BestThree(abilityScoreDistribution.Dexterity);
            AbilityScores.Constitution = dice.BestThree(abilityScoreDistribution.Constitution);
            AbilityScores.Intelligence = dice.BestThree(abilityScoreDistribution.Intelligence);
            AbilityScores.Wisdom = dice.BestThree(abilityScoreDistribution.Wisdom);
            AbilityScores.Charisma = dice.BestThree(abilityScoreDistribution.Charisma);

            Status = DetermineAbilityScoresStatus.CanCommitOrRollback;
        }

        /// <summary>
        /// Gives the amount of points that are left to distribute.
        /// </summary>
        /// <param name="abilityPoints">The points per ability.</param>
        /// <returns>The amount of points left.</returns>
        public int GetPointsLeft(AbilityValues abilityPoints)
        {
            CheckStatus("PointsLeft", DetermineAbilityScoresStatus.CanBuyAbilityScores);
            return (int)AbilityScorePoints - abilityPoints.AsEnumerable().Sum();
        }

        /// <summary>
        /// Get the ability scores for the given points.
        /// </summary>
        /// <param name="abilityPoints">The points for each ability.</param>
        /// <returns>The scores for each ability.</returns>
        public AbilityValues GetScoresForPoints(AbilityValues abilityPoints)
        {
            CheckStatus("ScoresForPoints", DetermineAbilityScoresStatus.CanBuyAbilityScores);
            if (abilityPoints.AsEnumerable().Min() < -4)
                throw new ArgumentException("Cannot give fewer than -4 points for any ability.");
            if (abilityPoints.AsEnumerable().Max() > 17)
                throw new ArgumentException("Cannot give more than 17 points for any ability.");

            var scores = new AbilityValues();
            scores.Strength = Constants.AbilityScoreCosts[abilityPoints.Strength];
            scores.Dexterity = Constants.AbilityScoreCosts[abilityPoints.Dexterity];
            scores.Constitution = Constants.AbilityScoreCosts[abilityPoints.Constitution];
            scores.Intelligence = Constants.AbilityScoreCosts[abilityPoints.Intelligence];
            scores.Wisdom = Constants.AbilityScoreCosts[abilityPoints.Wisdom];
            scores.Charisma = Constants.AbilityScoreCosts[abilityPoints.Charisma];
            return scores;            
        }

        /// <summary>
        /// Assign the ability scores according to the given points.
        /// </summary>
        /// <param name="abilityPoints">The points as distributed.</param>
        public void BuyScores(AbilityValues abilityPoints)
        {
            CheckStatus("BuyScores", DetermineAbilityScoresStatus.CanBuyAbilityScores);

            var pointsLeft = GetPointsLeft(abilityPoints);
            if (pointsLeft < 0)
                throw new ArgumentException("Too many points used.");
            // check for too many points left?

            var abilityScores = GetScoresForPoints(abilityPoints);
            CheckScoresBoundaries(abilityScores);

            AbilityScores = abilityScores;

            Status = DetermineAbilityScoresStatus.CanCommitOrRollback;
        }

        /// <summary>
        /// Assign the ability scores
        /// </summary>
        /// <param name="abilityScores"></param>
        public void AssignAbilityScores(AbilityValues abilityScores)
        {
            CheckStatus("AssignAbilityScores", DetermineAbilityScoresStatus.CanAssignScores);            

            if (AbilityScores != null)
                throw new InvalidOperationException("AbilityScores have already been assigned.");

            if (AssignableScores != null)
            {                
                foreach (int score in AssignableScores.Distinct())
                {
                    if (abilityScores.AsEnumerable().Count(s => s == score) != AssignableScores.Count(s => s == score))
                        throw new ArgumentException("The assigned scores are not equal to the assignable scores.");
                }
            }

            CheckScoresBoundaries(abilityScores);

            AbilityScores = abilityScores;
        }

        public void CommitAbilityScores()
        {
            CheckStatus("CommitAbilityScores", DetermineAbilityScoresStatus.CanCommitOrRollback);
            //TODO Write the ability scores somewhere.
        }
    }
}
